/*
 * Corridor.java
 * 
 * Created by demory on Mar 28, 2009, 5:39:30 PM
 * 
 * Copyright 2008 David D. Emory
 * 
 * This file is part of Five Points. See <http://www.fpdev.org> for
 * additional information regarding the project.
 * 
 * Five Points is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * Five Points is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with Five Points.  If not, see <http://www.gnu.org/licenses/>.
 */

package org.fpdev.apps.cart.network;

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.geom.AffineTransform;
import java.awt.geom.Line2D;
import java.awt.geom.Path2D;
import java.awt.geom.Point2D;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
import org.fpdev.util.FPUtil;
import org.fpdev.util.gui.MapCoordinates;
import org.fpdev.apps.cart.gui.map.Canvas;

/**
 *
 * @author demory
 */
public class Corridor {

  private int id_;
  private AnchorPoint fPoint_, tPoint_;

  private Set<Line> lines_;

  public Corridor(int id, AnchorPoint from, AnchorPoint to, boolean checkOrientation) {
    id_ = id;

    if(checkOrientation)
      setPoints(from, to);
    else {
      fPoint_ = from;
      tPoint_ = to;
    }

    fPoint_.registerCorridor(this);
    tPoint_.registerCorridor(this);

    lines_ = new HashSet<Line>();
  }

  private void setPoints(AnchorPoint from, AnchorPoint to) {
    Collection<Corridor> fCorrs = from.getCorridors();
    if(fCorrs.size() == 1) {
      Corridor adjacent = fCorrs.iterator().next();
      if(adjacent.fPoint() == from) {
        fPoint_ = from; tPoint_ = to;
        return;
      }
      else {
        fPoint_ = to; tPoint_ = from;
        return;
      }
    }

    Collection<Corridor> tCorrs = to.getCorridors();
    if(tCorrs.size() == 1) {
      Corridor adjacent = tCorrs.iterator().next();
      if(adjacent.fPoint() == to) {
        fPoint_ = to; tPoint_ = from;
        return;
      }
      else {
        fPoint_ = from; tPoint_ = to;
        return;
      }
    }


    fPoint_ = from;
    tPoint_ = to;

  }

  public int getID() {
    return id_;
  }

  public AnchorPoint fPoint() {
    return fPoint_;
  }

  public AnchorPoint tPoint() {
    return tPoint_;
  }

  public AnchorPoint opposite(AnchorPoint pt) {
    if(pt == fPoint_) return tPoint_;
    if(pt == tPoint_) return fPoint_;
    return null;
  }

  public Point2D.Double getElbow() {
    return getElbow(fPoint_.getX(), fPoint_.getY(), tPoint_.getX(), tPoint_.getY());
  }

  public Point2D.Double getElbow(double fx, double fy, double tx, double ty) {

    double dx = tx - fx;
    double dy = ty - fy;

    if(Math.abs(dx) > Math.abs(dy)) // more horizontal bounding box
      return new Point2D.Double(fx + (Math.abs(dx)/dx)*Math.abs(dy), ty);

    if(Math.abs(dx) < Math.abs(dy)) // more vertical bounding box
      return new Point2D.Double(tx, fy + (Math.abs(dy)/dy)*Math.abs(dx));

    return null;
  }

  public Point2D.Double nearestPoint(double x, double y) {
    Point2D.Double e = getElbow();
    Point2D.Double p1 = FPUtil.closestPointOnSegment(x, y, x1(), y1(), e.x, e.y);
    Point2D.Double p2 = FPUtil.closestPointOnSegment(x, y, e.x, e.y, x2(), y2());

    if(FPUtil.magnitude(x, y, p1.x, p1.y) < FPUtil.magnitude(x, y, p2.x, p2.y))
      return p1;
    else
      return p2;
  }

  public double x1() {
    return fPoint_.getX();
  }

  public double y1() {
    return fPoint_.getY();
  }

  public double x2() {
    return tPoint_.getX();
  }

  public double y2() {
    return tPoint_.getY();
  }

  public int getAngle(AnchorPoint pt) {
    if(pt == fPoint_) {
      if(isStraight()) return (int) (Math.toDegrees(FPUtil.getTheta(x2()-x1(), y2()-y1())) + 0.5);
      Point2D.Double e = getElbow();
      return (int) (Math.toDegrees(FPUtil.getTheta(e.x-x1(), e.y-y1())) + 0.5);
    }
    if(pt == tPoint_) {
      if(isStraight()) return (int) (Math.toDegrees(FPUtil.getTheta(x1()-x2(), y1()-y2())) + 0.5);
      Point2D.Double e = getElbow();
      return (int) (Math.toDegrees(FPUtil.getTheta(e.x-x2(), e.y-y2())) + 0.5);
    }
    
    return -1;
  }

  public boolean isStraight() {
    return (Math.abs(x2()-x1()) == Math.abs(y2()-y1()) || x1() == x2() || y1() == y2());
  }

  public boolean adjacentTo(Corridor c) {
    return fPoint_ == c.fPoint_ || fPoint_ == c.tPoint_ ||
           tPoint_ == c.fPoint_ || tPoint_ == c.tPoint_;
  }

  public boolean adjacentTo(AnchorPoint anchor) {
    return fPoint_ == anchor || tPoint_ == anchor;
  }

  public void flip() {
    AnchorPoint pt = fPoint_;
    fPoint_ = tPoint_;
    tPoint_ = pt;
  }

  public void unregisterFromEndpoints() {
    fPoint_.unregisterCorridor(this);
    tPoint_.unregisterCorridor(this);
  }

  public void registerLine(Line line) {
    lines_.add(line);
  }

  public void unregisterLine(Line line) {
    lines_.remove(line);
  }

  public Collection<Line> getLines() {
    return lines_;
  }

  public Path2D getPath(AnchorPoint fromPt, int offsetFrom, int offsetTo, MapCoordinates coords) {

    Path2D path = new Path2D.Double();
    boolean reverse = (fromPt != this.fPoint_);

    double x1 = fPoint_.getX();
    double y1 = fPoint_.getY();
    double x2 = tPoint_.getX();
    double y2 = tPoint_.getY();

    if(offsetFrom != 0 || offsetTo != 0) {
      // calculate offset distance in "world" units:
      double oFDistW = coords.dxToWorld(offsetFrom);
      double oTDistW = coords.dxToWorld(offsetTo);

      //System.out.println("offset "+offset+ " to " + distW);
      if(isStraight()) {

        // special case: to be handled later or (preferably) avoided completely?
        if(offsetFrom != offsetTo) {
          System.out.println("straight corridor with unequal offsets!");
          return path; // nothing drawn in this case (for now)
        }

        //System.out.println("applying straight offset");
        Line2D.Double vec = FPUtil.createNormalizedVector(fPoint_.getPoint2D(), tPoint_.getPoint2D(), 1);
        double dx = vec.x2 - vec.x1, dy = vec.y2-vec.y1;
        AffineTransform rot = AffineTransform. getRotateInstance(-Math.PI/2);
        Point2D.Double src = new Point2D.Double(dx, dy), dest = new Point2D.Double();
        rot.transform(src, dest);
        x1 += dest.x*oFDistW; y1 += dest.y*oFDistW;
        x2 += dest.x*oTDistW; y2 += dest.y*oTDistW;

      }
      else {
        //System.out.println("applying bent offset");
        Point2D.Double e = getElbow(x1, y1, x2, y2);
        Line2D.Double vec;
        double dx, dy;
        AffineTransform rot = AffineTransform. getRotateInstance(-Math.PI/2);
        Point2D.Double src, dest;

        // shift "from" point
        vec = FPUtil.createNormalizedVector(fPoint_.getPoint2D(), e, 1);
        dx = vec.x2 - vec.x1; dy = vec.y2-vec.y1;
        src = new Point2D.Double(dx, dy);
        dest = new Point2D.Double();
        rot.transform(src, dest);
        x1 += dest.x*oFDistW; y1 += dest.y*oFDistW;

        // shift "to" point
        vec = FPUtil.createNormalizedVector(e, tPoint_.getPoint2D(), 1);
        dx = vec.x2 - vec.x1; dy = vec.y2-vec.y1;
        src = new Point2D.Double(dx, dy);
        dest = new Point2D.Double();
        rot.transform(src, dest);
        x2 += dest.x*oTDistW; y2 += dest.y*oTDistW;

      }
    }

    if(reverse) path.moveTo(coords.xToScreen(x2), coords.yToScreen(y2));
    else path.moveTo(coords.xToScreen(x1), coords.yToScreen(y1));

    if(isStraight()) {
      /*int x1p = coords.xToScreen(x1), y1p = coords.yToScreen(y1);
      int x2p = coords.xToScreen(x2), y2p = coords.yToScreen(y2);
      drawExtendedLine(g2d, x1p, y1p, x2p, y2p);*/

      if(reverse) path.lineTo(coords.xToScreen(x1), coords.yToScreen(y1));
      else path.lineTo(coords.xToScreen(x2), coords.yToScreen(y2));

      return path;
    }

    Point2D.Double e = getElbow(x1, y1, x2, y2);

    double rw = 2;

    double theta = (3.0/8.0)*Math.PI;
    double l = rw / Math.tan(theta);

    double shortest = Math.min(FPUtil.magnitude(x1, y1, e.x, e.y), FPUtil.magnitude(x2, y2, e.x, e.y));
    if(shortest < l) {
      l = shortest;
      rw = l * Math.tan(theta);
    }

    Line2D.Double v1 = FPUtil.createNormalizedVector(e, new Point2D.Double(x1, y1), l);
    Line2D.Double v2 = FPUtil.createNormalizedVector(e, new Point2D.Double(x2, y2), l);

    /*int ccw = Line2D.relativeCCW(x1, y1, e.x, e.y, x2, y2);
    theta = -ccw * Math.PI/2;

    AffineTransform at1 = AffineTransform. getRotateInstance(theta, v1.x2, v1.y2);
    Point2D.Double eprime1 = new Point2D.Double();
    at1.transform(e, eprime1);*/

    if(reverse) {
      path.lineTo(coords.xToScreen(v2.x2), coords.yToScreen(v2.y2));
      path.quadTo(coords.xToScreen(e.x), coords.yToScreen(e.y), coords.xToScreen(v1.x2), coords.yToScreen(v1.y2));
      path.lineTo(coords.xToScreen(x1), coords.yToScreen(y1));
    }
    else {
      path.lineTo(coords.xToScreen(v1.x2), coords.yToScreen(v1.y2));
      path.quadTo(coords.xToScreen(e.x), coords.yToScreen(e.y), coords.xToScreen(v2.x2), coords.yToScreen(v2.y2));
      path.lineTo(coords.xToScreen(x2), coords.yToScreen(y2));
    }

    return path;
  }

  public void draw(Canvas canvas) {
    draw(canvas, 0, 0, new SubStyle(2, Color.gray));
  }
  
  public void draw(Canvas canvas, int offsetFrom, int offsetTo, SubStyle sstyle) { //int width, int offsetFrom, int offsetTo, Color color) {
    draw(canvas.getGraphics2D(), canvas.getCoordinates(), offsetFrom, offsetTo, sstyle); //2, color);
  }

  public void draw(Graphics2D g2d, MapCoordinates coords, int offsetFrom, int offsetTo, SubStyle sstyle) { //int width, Color color) {
    //Graphics2D g2d = canvas.getGraphics2D();
    g2d.setColor(sstyle.getColor());
    g2d.setStroke(new BasicStroke(sstyle.getWidth(), BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER));

    double x1 = fPoint_.getX();
    double y1 = fPoint_.getY();
    double x2 = tPoint_.getX();
    double y2 = tPoint_.getY();

    //MapCoordinates coords = canvas.getCoordinates();

    if(offsetFrom != 0 || offsetTo != 0) {
      // calculate offset distance in "world" units:
      double oFDistW = coords.dxToWorld(offsetFrom);
      double oTDistW = coords.dxToWorld(offsetTo);

      //System.out.println("offset "+offset+ " to " + distW);
      if(isStraight()) {

        // special case: to be handled later or (preferably) avoided completely?
        if(offsetFrom != offsetTo) {
          //System.out.println("straight corridor with unequal offsets!");
          return; // nothing drawn in this case (for now)
        }

        //System.out.println("applying straight offset");
        Line2D.Double vec = FPUtil.createNormalizedVector(fPoint_.getPoint2D(), tPoint_.getPoint2D(), 1);
        double dx = vec.x2 - vec.x1, dy = vec.y2-vec.y1;
        AffineTransform rot = AffineTransform. getRotateInstance(-Math.PI/2);
        Point2D.Double src = new Point2D.Double(dx, dy), dest = new Point2D.Double();
        rot.transform(src, dest);
        x1 += dest.x*oFDistW; y1 += dest.y*oFDistW;
        x2 += dest.x*oTDistW; y2 += dest.y*oTDistW;

      }
      else {
        //System.out.println("applying bent offset");
        Point2D.Double e = getElbow(x1, y1, x2, y2);
        Line2D.Double vec;
        double dx, dy;
        AffineTransform rot = AffineTransform. getRotateInstance(-Math.PI/2);
        Point2D.Double src, dest;

        // shift "from" point
        vec = FPUtil.createNormalizedVector(fPoint_.getPoint2D(), e, 1);
        dx = vec.x2 - vec.x1; dy = vec.y2-vec.y1;
        src = new Point2D.Double(dx, dy);
        dest = new Point2D.Double();
        rot.transform(src, dest);
        x1 += dest.x*oFDistW; y1 += dest.y*oFDistW;

        // shift "to" point
        vec = FPUtil.createNormalizedVector(e, tPoint_.getPoint2D(), 1);
        dx = vec.x2 - vec.x1; dy = vec.y2-vec.y1;
        src = new Point2D.Double(dx, dy);
        dest = new Point2D.Double();
        rot.transform(src, dest);
        x2 += dest.x*oTDistW; y2 += dest.y*oTDistW;

      }
    }

    
    if(isStraight()) {
      int x1p = coords.xToScreen(x1), y1p = coords.yToScreen(y1);
      int x2p = coords.xToScreen(x2), y2p = coords.yToScreen(y2);
      drawExtendedLine(g2d, x1p, y1p, x2p, y2p);
      return;
    }

    Point2D.Double e = getElbow(x1, y1, x2, y2);

    double rw = 2;

    double theta = (3.0/8.0)*Math.PI;
    double l = rw / Math.tan(theta);

    double shortest = Math.min(FPUtil.magnitude(x1, y1, e.x, e.y), FPUtil.magnitude(x2, y2, e.x, e.y));
    if(shortest < l) {
      l = shortest;
      rw = l * Math.tan(theta);
    }

    Line2D.Double v1 = FPUtil.createNormalizedVector(e, new Point2D.Double(x1, y1), l);
    Line2D.Double v2 = FPUtil.createNormalizedVector(e, new Point2D.Double(x2, y2), l);

    int ccw = Line2D.relativeCCW(x1, y1, e.x, e.y, x2, y2);
    theta = -ccw * Math.PI/2;

    AffineTransform at1 = AffineTransform. getRotateInstance(theta, v1.x2, v1.y2);
    Point2D.Double eprime1 = new Point2D.Double();
    at1.transform(e, eprime1);

    Line2D.Double rVect1 = FPUtil.createNormalizedVector(v1.getP2(), eprime1, rw);

    int startAngle = (int) Math.toDegrees(FPUtil.getTheta(rVect1.x1-rVect1.x2, rVect1.y1-rVect1.y2));

    
    drawExtendedLine(g2d, coords.xToScreen(x1), coords.yToScreen(y1),
                 coords.xToScreen(v1.x2), coords.yToScreen(v1.y2));
    drawExtendedLine(g2d, coords.xToScreen(v2.x2), coords.yToScreen(v2.y2),
                 coords.xToScreen(x2), coords.yToScreen(y2));

    //g2d.drawLine(coords.xToScreen(v1.x2), coords.yToScreen(v1.y2), coords.xToScreen(v2.x2), coords.yToScreen(v2.y2));
    int rg = coords.distToScreen(rw);
    int cx = coords.xToScreen(rVect1.getX2());
    int cy = coords.yToScreen(rVect1.getY2());

    //g2d.setStroke(new BasicStroke(width, BasicStroke.CAP_ROUND, BasicStroke.JOIN_MITER));
    //g2d.drawArc(cx-rg, cy-rg, rg*2, rg*2, startAngle, -ccw*45);

    g2d.setStroke(new BasicStroke(sstyle.getWidth(), BasicStroke.CAP_ROUND, BasicStroke.JOIN_MITER));
    drawArc(g2d, coords, rVect1.getX2(), rVect1.getY2(), rw, startAngle, -ccw*45);
  }

  public void drawArc(Graphics2D g2d, MapCoordinates coords, double cx, double cy, double r, double start, double span) {
    double x1 = cx + Math.cos(Math.toRadians(start))*r;
    double y1 = cy + Math.sin(Math.toRadians(start))*r;


    if(span > 0) {
      for(double a = start; a <= start+span; a += span/8) {
        double x2 = cx + Math.cos(Math.toRadians(a))*r;
        double y2 = cy + Math.sin(Math.toRadians(a))*r;
        g2d.drawLine(coords.xToScreen(x1), coords.yToScreen(y1), coords.xToScreen(x2), coords.yToScreen(y2));
        x1 = x2; y1 = y2;
      }
    }
    else {
      for(double a = start; a >= start+span; a += span/8) {
        double x2 = cx + Math.cos(Math.toRadians(a))*r;
        double y2 = cy + Math.sin(Math.toRadians(a))*r;
        g2d.drawLine(coords.xToScreen(x1), coords.yToScreen(y1), coords.xToScreen(x2), coords.yToScreen(y2));
        x1 = x2; y1 = y2;
      }
    }
    //g2d.drawLine(coords.xToScreen(x1), coords.yToScreen(y1), coords.xToScreen(x2), coords.yToScreen(y2));

  }

  public void drawExtendedLine(Graphics2D g2d, int x1, int y1, int x2, int y2) {
    if(x1 < x2) {
      x1--;
      x2++;
    }
    else if(x2 < x1) {
      x2--;
      x1++;
    }

    if(y1 < y2) {
      y1--;
      y2++;
    }
    else if(y2 < y1) {
      y2--;
      y1++;
    }

    g2d.drawLine(x1, y1, x2, y2);

  }

  public String getXML() {
    return "<corridor id=\""+id_+"\" fpoint=\""+fPoint_.getID()+"\" tpoint=\""+tPoint_.getID()+"\" />\n";
  }
}
